Location: PHPKode > projects > Maintainable PHP Framework > vendor/Mad/Support/ArrayConversion.php
<?php
/**
 * @category   Mad
 * @package    Mad_Support
 * @copyright  (c) 2007-2009 Maintainable Software, LLC
 * @license    http://opensource.org/licenses/bsd-license.php BSD
 */

/**
 * The base object from which all DataObjects are extended from
 *
 * @category   Mad
 * @package    Mad_Support
 * @copyright  (c) 2007-2009 Maintainable Software, LLC
 * @license    http://opensource.org/licenses/bsd-license.php BSD
 */
class Mad_Support_ArrayConversion
{
    public $xmlTypeNames = array(
      "integer" => "integer", 
      "double"  => "float", 
      "boolean" => "boolean"
    );

    public $xmlFormatting = array(
      "boolean"  => 'formatBoolean',
      "binary"   => 'formatBinary',
      "date"     => 'formatDate',
      "datetime" => 'formatDatetime',
      "yaml"     => 'formatYaml'
    );
    
    public $xmlParsing = array(
      "symbol"       => 'parseSymbol',
      "date"         => 'parseDate',
      "datetime"     => 'parseDatetime',
      "dateTime"     => 'parseDatetime',
      "integer"      => 'parseInteger',
      "float"        => 'parseFloat',
      "double"       => 'parseDouble',
      "decimal"      => 'parseDecimal',
      "boolean"      => 'parseBoolean',
      "string"       => 'parseString',
      "yaml"         => 'parseYaml',
      "base64Binary" => 'parseBase64Binary',
      "file"         => 'parseFile',
    );
    
    /**
     * Convert an array to XML. While PHP has a single array() to do 
     * ordered and associative collections, we've split toXml into two
     * separate methods to resemble the Rails code more closely
     * 
     * @param   array   $array
     * @param   array   $options
     */
    public function toXml($array, $options = array()) 
    {
        // associative
        if (!empty($array) && !$array instanceof Mad_Model_Collection && !is_int(key($array))) {
            return $this->hashToXml($array, $options);

        // numeric
        } else {
            return $this->arrayToXml($array, $options);
        }
    }


    /**
     * Convert an XML string to an associative array
     * 
     * @param   string  $xmlStr
     * @return  array
     */
    public function fromXml($xmlStr)
    {
        // build array data struct from the xml
        $xml = new SimpleXMLElement($xmlStr);
        $params = $this->_parseElement($xml);

        // remove dasherized keys and typecast values
        $params = $this->_undasherizeKeys($params);
        return $this->_typecastXmlValue($params);
    }

    /**
     * Convert an associative array to XML
     * 
     * @todo - complete this!
     * @param   array   $options
     * @return  string
     */
    public function hashToXml($hash, $options = array())
    {  
        if (!isset($options['indent'])) { $options['indent'] = 2; }
        if (!isset($options['root']))   { $options['root']   = 'hash'; }

        if (empty($options['builder'])) {
            $options['builder'] = new Mad_Support_Builder(
                array('indent' => $options['indent']));
        }
        if (empty($options['skipInstruct'])) {
            $options['builder']->instruct(); 
        }
        $dasherize = !array_key_exists('dasherize', $options) || !empty($options['dasherize']);
        $root = $dasherize ? Mad_Support_Inflector::dasherize($options['root']) : $options['root'];

        $tag = $options['builder']->startTag($root); 
        foreach ($hash as $key => $value) {
            // associative array
            if (is_array($value) && !is_int(key($value))) {
                $opts = array_merge($options, array('root' => $key, 'skipInstruct' => true));
                $this->hashToXml($value, $opts);

            // array
            } elseif (is_array($value)) {
                $opts = array_merge($options, array('children'     => Mad_Support_Inflector::singularize($key), 
                                                    'root'         => $key, 
                                                    'skipInstruct' => true));
                $this->arrayToXml($value, $opts);

            } else {
                // object
                if (is_object($value) && is_callable(array($value, 'toXml'))) {
                    $opts = array_merge($options, array('root' => $key, 'skipInstruct' => true));
                    $value->toXml($opts);

                // object without toXml
                } elseif (is_object($value)) {
                    throw new Mad_Support_Exception("Not all elements respond to toXml");

                // native type
                } else {
                    if (isset($this->xmlTypeNames[gettype($value)])) {
                        $typeName = $this->xmlTypeNames[gettype($value)];
                    } else {
                        $typeName = null;
                    }
                    $key = $dasherize ? Mad_Support_Inflector::dasherize($key) : $key;
                    
                    if (!empty($options['skipTypes']) || $value === null || $typeName === null) {
                        $attributes = array();
                    } else {
                        $attributes = array('type' => $typeName);
                    }

                    if ($value === null) { $attributes['nil'] = 'true'; }

                    if (isset($this->xmlFormatting[$typeName])) {
                        $formatter = $this->xmlFormatting[$typeName];
                        $value = $value ? $this->{$formatter}($value) : null;
                    }

                    $options['builder']->tag($key, $value, $attributes);
                }
            }
            
        }
        $tag->end();
        return $options['builder']->__toString();
    }

    /**
     * Convert array collection to XML
     * @param   array   $array
     * @param   array   $options
     */
    public function arrayToXml($array, $options = array())
    {
        $firstElt  = current($array);
        $firstType = is_object($firstElt) ? get_class($firstElt) : gettype($firstElt);
        $sameTypes = true;

        foreach ($array as $element) {
            // either an array or object with toXml method
            if (!is_array($element) && !is_callable(array($element, 'toXml'))) {
                throw new Mad_Support_Exception("Not all elements respond to toXml");
            }
            if (get_class($element) != $firstType) { $sameTypes = false; }
        }

        if (!isset($options['root'])) {
            if ($sameTypes && count($array) > 0) {
                $options['root'] = Mad_Support_Inflector::pluralize($firstType);
            } else {
                $options['root'] = 'records';
            }
        }
        if (!isset($options['children'])) {
            $options['children'] = Mad_Support_Inflector::singularize($options['root']);
        }

        if (!isset($options['indent']))    { $options['indent']    = 2; }
        if (!isset($options['skipTypes'])) { $options['skipTypes'] = false; }

        if (empty($options['builder'])) {
            $options['builder'] = new Mad_Support_Builder(
                array('indent' => $options['indent']));
        }

        $root = $options['root'];
        unset($options['root']);
        
        $children = $options['children'];
        unset($options['children']);

        if (!array_key_exists('dasherize', $options) || !empty($options['dasherize'])) {
            $root = Mad_Support_Inflector::dasherize($root);
        }
        if (empty($options['skipInstruct'])) {
            $options['builder']->instruct(); 
        }

        $opts = array_merge($options, array('root' => $children));

        $builder = $options['builder'];
        $attrs   = $options['skipTypes'] ? array() : array('type' => 'array');
        
        // no elements in array
        if (count($array) == 0) {
            $builder->tag($root, '', $attrs);
            
        // build xml from elements
        } else {
            $tag = $builder->startTag($root, '', $attrs);
                $opts['skipInstruct'] = true;
                foreach ($array as $element) {
                    // associative array
                    if (is_array($element) && !is_int(key($element))) {
                        $this->hashToXml($element, $opts);
                    // array
                    } elseif (is_array($element)) {
                        $this->arrayToXml($element, $opts);
                    // object
                    } else {
                        $element->toXml($opts);
                    }
                }
            $tag->end();
        }
        return $builder->__toString();        
    }


    // formatting

    public function formatBoolean($boolean)
    {
        return $boolean ? 'true' : 'false';
    }
    
    public function formatBinary($binary)
    {
        return base64_encode($binary);
    }
    
    public function formatYaml($yaml)
    {
        return Horde_Yaml::dump($yaml);
    }
    
    public function formatDate($date)
    {
        // 0000-00-00 becomes NULL (http://bugs.php.net/bug.php?id=45647)
        if (preg_replace('/[^\d]/', '', $date) == 0) { return null; }

        $formatted = gmdate('Y-m-d', strtotime($date));
        return $formatted == '1970-01-01' ? null : $formatted;
    }
    
    public function formatDatetime($date)
    {
        // 0000-00-00 00:00:00 becomes NULL (http://bugs.php.net/bug.php?id=45647)
        if (preg_replace('/[^\d]/', '', $date) == 0) { return null; }

        $formatted = gmdate('c', strtotime($date));
        return substr($formatted, 0, 10) == '1970-01-01' ? null : $formatted;
    }


    // parsing

    public function parseSymbol($symbol, $entity = null)
    {
        return (string)$symbol;
    }

    public function parseDate($date, $entity = null)
    {
        // 0000-00-00 becomes NULL (http://bugs.php.net/bug.php?id=45647)
        if (preg_replace('/[^\d]/', '', $date) == 0) { return null; }

        // check if the date is valid
        $parsed = gmdate("Y-m-d", strtotime($date));
        if ($parsed == '1970-01-01') {
            return null;

        // of it's valid - return local time
        } else {
            return date("Y-m-d", strtotime($parsed));
        }
    }
    
    public function parseDatetime($datetime, $entity = null)
    {
        // 0000-00-00 00:00:00 becomes NULL (http://bugs.php.net/bug.php?id=45647)
        if (preg_replace('/[^\d]/', '', $datetime) == 0) { return null; }

        // check if the date is valid
        $parsed = gmdate("Y-m-d H:i:s", strtotime($datetime));
        if (substr($parsed, 0, 10) == '1970-01-01') {
            return null;

        // of it's valid - return local time
        } else {
            return date("Y-m-d H:i:s", strtotime($datetime));
        }
    }
    
    /**
     * Ruby has Fixnum and Bignum, and automatically converts between them.
     * PHP only has one Integer type and its maximum is determined by the
     * platform PHP is compiled for.  This causes a behavioral difference 
     * between this PHP version and its Rails counterpart.
     * 
     * On 32-bit PHP, the maximum Integer is 2147483647.  We throw an
     * Overflow exception if the XML to deserialize specifies an Integer
     * larger than this maximum.  On 64-bit PHP, we do not have this limit
     * and parsing an Integer works the same as the Rails version.
     */
    public function parseInteger($integer, $entity = null)
    {
        if ($integer == '') { return null; }

        $typecast  = (int)$integer;

        // check overflow
        $recast = (string)$typecast;
        if ($recast != $integer) {
            $msg = "String \"$integer\" was cast to Integer $typecast"; 
            throw new OverflowException($msg);
        }

        return $typecast;
    }
    
    public function parseFloat($float, $entity = null)
    {
        return (float)$float;
    }
    
    public function parseDouble($double, $entity = null)
    {
        return (double)$double;
    }
    
    public function parseDecimal($decimal, $entity = null)
    {
        return (float)$decimal;
    }
    
    public function parseBoolean($boolean, $entity = null)
    {
        if ($boolean == 'true') {
            return true;
        } elseif ($boolean == 'false') {
            return false;
        } else {
            return (boolean)$boolean;
        }
    }

    public function parseString($string, $entity = null)
    {
        return (string)$string;
    }

    public function parseYaml($yaml, $entity = null)
    {
        if (empty($yaml)) { return null; }

        return Horde_Yaml::load($yaml);
    }

    public function parseBase64Binary($bin, $entity = null)
    {
        return base64_decode($bin);
    }

    public function parseFile($file, $entity = null)
    {
        // default for name/content-type
        if (empty($entity['name'])) { 
            $entity['name'] = 'untitled'; 
        }
        if (empty($entity['content_type'])) { 
            $entity['content_type'] = 'application/octet-stream'; 
        }

        // Make an object that is polymorphic with Mad_Controller_File_Upload
        $path     = tempnam("/tmp", $entity['name']);
        $contents = base64_decode($file);
        file_put_contents($path, $contents);

        $file = array(
            'originalFilename' => $entity['name'], 
            'contentType'      => $entity['content_type'], 
            'length'           => filesize($path),
            'path'             => $path
        );
        return (object)$file;
    }

    
    // Protected
    
    /**
     * Create an array similar to that returned by xmlsimple in Ruby
     */
    protected function _parseElement($element)
    {
        $name    = $element->getName();
        $text    = preg_replace("/^\s+$/", "", (string)$element);
        $content = $text !== '' ? array('__content__' => $text) : array();

        $attrs = array();
        foreach ($element->attributes() as $key => $val) {
            $attrs[$key] = (string)$val;
        }

        $children = array();
        foreach ($element->children() as $child) {
            $childData = $this->_parseElement($child);
            $childName = $child->getName();

            if (isset($children[$childName])) {
                $children[$childName]   = array($children[$childName]);
                $children[$childName][] = $childData[$childName];
            } else {
                $children[$childName] = $childData[$childName];
            }
        }

        return array($name => array_merge($attrs, $content, $children));
    }
    
    /**
     * Typecast values based on their specified type
     * 
     * @param   array   $value
     * @return  array
     */
    protected function _typecastXmlValue($value)
    {
        // associative array
        if (is_array($value) && !is_int(key($value))) {
            // collection
            if (isset($value['type']) && $value['type'] == 'array') {
                $entries = array();
                foreach ($value as $k => $v) {
                    if ($k != 'type') { $entries[] =  $v; }
                }
                $entries = current($entries);

                // empty
                if (empty($entries) || (isset($value['__content__']) && empty($value['__content__']))) {
                    return array();

                } else {
                    // array
                    if (is_array($entries) && is_int(key($entries))) {
                        $result = array();
                        foreach ($entries as $v) {
                            $result[] = $this->_typecastXmlValue($v);
                        }
                        return $result;

                    // associative array
                    } elseif (is_array($entries)) {
                        return array($this->_typecastXmlValue($entries));
                    
                    // error
                    } else {
                        throw new Mad_Support_Exception("can't typecast ".gettype($entries));
                    }
                }

            // content
            } elseif (array_key_exists('__content__', $value)) {
                $type = isset($value['type']) ? $value['type'] : 'string';

                // parse data type
                if (isset($this->xmlParsing[$type])) {
                    $parser = $this->xmlParsing[$type];                    
                    return $this->$parser($value['__content__'], $value);

                } else {
                    return $value['__content__'];
                }

            // empty string
            } elseif ((isset($value['type']) && $value['type'] == 'string') && 
                      (empty($value['nil'])  || $value['nil'] != 'true')) {
                return '';

            // blank or nil parsed values are represented by null
            } elseif (empty($value) || (isset($value['nil']) && $value['nil'] == 'true')) {
                return null;

            // If the type is the only element which makes it then 
            // this still makes the value null, except if type is
            // a XML node(where type['value'] is a Hash)
            } elseif (isset($value['type']) && count($value) == 1 && !is_array($value['type'])) {
                return null;

            } else {
                $xmlValue = array();
                foreach ($value as $k => $v) {
                    $xmlValue[$k] = $this->_typecastXmlValue($v);
                }

                // Turn array('files' => array('file' => Object)) into 
                //      array('files' => Object) so it is compatible with
                // how multipart uploaded files from HTML appear
                if (isset($xmlValue["file"]) && is_object($xmlValue["file"])) {
                    return $xmlValue["file"];
                } else {
                    return $xmlValue;
                }
            }

        // array
        } elseif (is_array($value)) {
            $vals = array();
            foreach ($value as $val) {
                $vals[] = $this->_typecastXmlValue($val);
            }

            if (count($vals) == 0) {
                return null;
            } elseif (count($vals) == 1) {
                return current($vals);
            } else {
                return $vals;
            }

        // string
        } elseif (is_string($value)) {
            return $value;
            
        // error
        }  else {
            throw new Mad_Support_Exception("can't typecast ".gettype($value)." - $value");
        }
    }
    
    /**
     * Change all dashes to underscores in keys
     * 
     * @param   array   $params
     * @return  array
     */
    protected function _undasherizeKeys($params)
    {
        // associative array
        if (is_array($params) && !is_int(key($params))) {
            $result = array();
            foreach ($params as $k => $v) {
                $result[strtr($k, '-', '_')] = $this->_undasherizeKeys($v);
            }
            return $result;

        // array
        } elseif (is_array($params)) {
            $results = array();
            foreach ($params as $v) {
                $results[] = $this->_undasherizeKeys($v);
            }
            return $results;

        } else {
            return $params;
        }
    }
}
Return current item: Maintainable PHP Framework