      1: <?php
      2:
      3: defined('BASEPATH') OR exit('No direct script access allowed');
      4:
      5: /**
      6:  * Format class
      7:  * Help convert between various formats such as XML, JSON, CSV, etc.
      8:  *
      9:  * @author    Phil Sturgeon, Chris Kacerguis, @softwarespot
     10:  * @license   http://www.dbad-license.org/
     11:  */
     12: class Format {
     13:
     14:     /**
     15:      * Array output format
     16:      */
     17:     const ARRAY_FORMAT = 'array';
     18:
     19:     /**
     20:      * Comma Separated Value (CSV) output format
     21:      */
     22:     const CSV_FORMAT = 'csv';
     23:
     24:     /**
     25:      * Json output format
     26:      */
     27:     const JSON_FORMAT = 'json';
     28:
     29:     /**
     30:      * HTML output format
     31:      */
     32:     const HTML_FORMAT = 'html';
     33:
     34:     /**
     35:      * PHP output format
     36:      */
     37:     const PHP_FORMAT = 'php';
     38:
     39:     /**
     40:      * Serialized output format
     41:      */
     42:     const SERIALIZED_FORMAT = 'serialized';
     43:
     44:     /**
     45:      * XML output format
     46:      */
     47:     const XML_FORMAT = 'xml';
     48:
     49:     /**
     50:      * Default format of this class
     51:      */
     52:     const DEFAULT_FORMAT = self::JSON_FORMAT; // Couldn't be DEFAULT, as this is a keyword
     53:
     54:     /**
     55:      * CodeIgniter instance
     56:      *
     57:      * @var object
     58:      */
     59:     private $_CI;
     60:
     61:     /**
     62:      * Data to parse
     63:      *
     64:      * @var mixed
     65:      */
     66:     protected $_data = [];
     67:
     68:     /**
     69:      * Type to convert from
     70:      *
     71:      * @var string
     72:      */
     73:     protected $_from_type = NULL;
     74:
     75:     /**
     76:      * DO NOT CALL THIS DIRECTLY, USE factory()
     77:      *
     78:      * @param NULL $data
     79:      * @param NULL $from_type
     80:      * @throws Exception
     81:      */
     82:
     83:     public function __construct($data = NULL, $from_type = NULL)
     84:     {
     85:         // Get the CodeIgniter reference
     86:         $this->_CI = &get_instance();
     87:
     88:         // Load the inflector helper
     89:         $this->_CI->load->helper('inflector');
     90:
     91:         // If the provided data is already formatted we should probably convert it to an array
     92:         if ($from_type !== NULL)
     93:         {
     94:             if (method_exists($this, '_from_'.$from_type))
     95:             {
     96:                 $data = call_user_func([$this, '_from_'.$from_type], $data);
     97:             }
     98:             else
     99:             {
    100:                 throw new Exception('Format class does not support conversion from "'.$from_type.'".');
    101:             }
    102:         }
    103:
    104:         // Set the member variable to the data passed
    105:         $this->_data = $data;
    106:     }
    107:
    108:     /**
    109:      * Create an instance of the format class
    110:      * e.g: echo $this->format->factory(['foo' => 'bar'])->to_csv();
    111:      *
    112:      * @param mixed $data Data to convert/parse
    113:      * @param string $from_type Type to convert from e.g. json, csv, html
    114:      *
    115:      * @return object Instance of the format class
    116:      */
    117:     public function factory($data, $from_type = NULL)
    118:     {
    119:         // $class = __CLASS__;
    120:         // return new $class();
    121:
    122:         return new static($data, $from_type);
    123:     }
    124:
    125:     // FORMATTING OUTPUT ---------------------------------------------------------
    126:
    127:     /**
    128:      * Format data as an array
    129:      *
    130:      * @param mixed|NULL $data Optional data to pass, so as to override the data passed
    131:      * to the constructor
    132:      * @return array Data parsed as an array; otherwise, an empty array
    133:      */
    134:     public function to_array($data = NULL)
    135:     {
    136:         // If no data is passed as a parameter, then use the data passed
    137:         // via the constructor
    138:         if ($data === NULL && func_num_args() === 0)
    139:         {
    140:             $data = $this->_data;
    141:         }
    142:
    143:         // Cast as an array if not already
    144:         if (is_array($data) === FALSE)
    145:         {
    146:             $data = (array) $data;
    147:         }
    148:
    149:         $array = [];
    150:         foreach ((array) $data as $key => $value)
    151:         {
    152:             if (is_object($value) === TRUE || is_array($value) === TRUE)
    153:             {
    154:                 $array[$key] = $this->to_array($value);
    155:             }
    156:             else
    157:             {
    158:                 $array[$key] = $value;
    159:             }
    160:         }
    161:
    162:         return $array;
    163:     }
    164:
    165:     /**
    166:      * Format data as XML
    167:      *
    168:      * @param mixed|NULL $data Optional data to pass, so as to override the data passed
    169:      * to the constructor
    170:      * @param NULL $structure
    171:      * @param string $basenode
    172:      * @return mixed
    173:      */
    174:     public function to_xml($data = NULL, $structure = NULL, $basenode = 'xml')
    175:     {
    176:         if ($data === NULL && func_num_args() === 0)
    177:         {
    178:             $data = $this->_data;
    179:         }
    180:
    181:         // turn off compatibility mode as simple xml throws a wobbly if you don't.
    182:         if (ini_get('zend.ze1_compatibility_mode') == 1)
    183:         {
    184:             ini_set('zend.ze1_compatibility_mode', 0);
    185:         }
    186:
    187:         if ($structure === NULL)
    188:         {
    189:             $structure = simplexml_load_string("<?xml version='1.0' encoding='utf-8'?><$basenode />");
    190:         }
    191:
    192:         // Force it to be something useful
    193:         if (is_array($data) === FALSE && is_object($data) === FALSE)
    194:         {
    195:             $data = (array) $data;
    196:         }
    197:
    198:         foreach ($data as $key => $value)
    199:         {
    200:
    201:             //change false/true to 0/1
    202:             if (is_bool($value))
    203:             {
    204:                 $value = (int) $value;
    205:             }
    206:
    207:             // no numeric keys in our xml please!
    208:             if (is_numeric($key))
    209:             {
    210:                 // make string key...
    211:                 $key = (singular($basenode) != $basenode) ? singular($basenode) : 'item';
    212:             }
    213:
    214:             // replace anything not alpha numeric
    215:             $key = preg_replace('/[^a-z_\-0-9]/i', '', $key);
    216:
    217:             if ($key === '_attributes' && (is_array($value) || is_object($value)))
    218:             {
    219:                 $attributes = $value;
    220:                 if (is_object($attributes))
    221:                 {
    222:                     $attributes = get_object_vars($attributes);
    223:                 }
    224:
    225:                 foreach ($attributes as $attribute_name => $attribute_value)
    226:                 {
    227:                     $structure->addAttribute($attribute_name, $attribute_value);
    228:                 }
    229:             }
    230:             // if there is another array found recursively call this function
    231:             elseif (is_array($value) || is_object($value))
    232:             {
    233:                 $node = $structure->addChild($key);
    234:
    235:                 // recursive call.
    236:                 $this->to_xml($value, $node, $key);
    237:             }
    238:             else
    239:             {
    240:                 // add single node.
    241:                 $value = htmlspecialchars(html_entity_decode($value, ENT_QUOTES, 'UTF-8'), ENT_QUOTES, 'UTF-8');
    242:
    243:                 $structure->addChild($key, $value);
    244:             }
    245:         }
    246:
    247:         return $structure->asXML();
    248:     }
    249:
    250:     /**
    251:      * Format data as HTML
    252:      *
    253:      * @param mixed|NULL $data Optional data to pass, so as to override the data passed
    254:      * to the constructor
    255:      * @return mixed
    256:      */
    257:     public function to_html($data = NULL)
    258:     {
    259:         // If no data is passed as a parameter, then use the data passed
    260:         // via the constructor
    261:         if ($data === NULL && func_num_args() === 0)
    262:         {
    263:             $data = $this->_data;
    264:         }
    265:
    266:         // Cast as an array if not already
    267:         if (is_array($data) === FALSE)
    268:         {
    269:             $data = (array) $data;
    270:         }
    271:
    272:         // Check if it's a multi-dimensional array
    273:         if (isset($data[0]) && count($data) !== count($data, COUNT_RECURSIVE))
    274:         {
    275:             // Multi-dimensional array
    276:             $headings = array_keys($data[0]);
    277:         }
    278:         else
    279:         {
    280:             // Single array
    281:             $headings = array_keys($data);
    282:             $data = [$data];
    283:         }
    284:
    285:         // Load the table library
    286:         $this->_CI->load->library('table');
    287:
    288:         $this->_CI->table->set_heading($headings);
    289:
    290:         foreach ($data as $row)
    291:         {
    292:             // Suppressing the "array to string conversion" notice
    293:             // Keep the "evil" @ here
    294:             $row = @array_map('strval', $row);
    295:
    296:             $this->_CI->table->add_row($row);
    297:         }
    298:
    299:         return $this->_CI->table->generate();
    300:     }
    301:
    302:     /**
    303:      * @link http://www.metashock.de/2014/02/create-csv-file-in-memory-php/
    304:      * @param mixed|NULL $data Optional data to pass, so as to override the data passed
    305:      * to the constructor
    306:      * @param string $delimiter The optional delimiter parameter sets the field
    307:      * delimiter (one character only). NULL will use the default value (,)
    308:      * @param string $enclosure The optional enclosure parameter sets the field
    309:      * enclosure (one character only). NULL will use the default value (")
    310:      * @return string A csv string
    311:      */
    312:     public function to_csv($data = NULL, $delimiter = ',', $enclosure = '"')
    313:     {
    314:         // Use a threshold of 1 MB (1024 * 1024)
    315:         $handle = fopen('php://temp/maxmemory:1048576', 'w');
    316:         if ($handle === FALSE)
    317:         {
    318:             return NULL;
    319:         }
    320:
    321:         // If no data is passed as a parameter, then use the data passed
    322:         // via the constructor
    323:         if ($data === NULL && func_num_args() === 0)
    324:         {
    325:             $data = $this->_data;
    326:         }
    327:
    328:         // If NULL, then set as the default delimiter
    329:         if ($delimiter === NULL)
    330:         {
    331:             $delimiter = ',';
    332:         }
    333:
    334:         // If NULL, then set as the default enclosure
    335:         if ($enclosure === NULL)
    336:         {
    337:             $enclosure = '"';
    338:         }
    339:
    340:         // Cast as an array if not already
    341:         if (is_array($data) === FALSE)
    342:         {
    343:             $data = (array) $data;
    344:         }
    345:
    346:         // Check if it's a multi-dimensional array
    347:         if (isset($data[0]) && count($data) !== count($data, COUNT_RECURSIVE))
    348:         {
    349:             // Multi-dimensional array
    350:             $headings = array_keys($data[0]);
    351:         }
    352:         else
    353:         {
    354:             // Single array
    355:             $headings = array_keys($data);
    356:             $data = [$data];
    357:         }
    358:
    359:         // Apply the headings
    360:         fputcsv($handle, $headings, $delimiter, $enclosure);
    361:
    362:         foreach ($data as $record)
    363:         {
    364:             // If the record is not an array, then break. This is because the 2nd param of
    365:             // fputcsv() should be an array
    366:             if (is_array($record) === FALSE)
    367:             {
    368:                 break;
    369:             }
    370:
    371:             // Suppressing the "array to string conversion" notice.
    372:             // Keep the "evil" @ here.
    373:             $record = @ array_map('strval', $record);
    374:
    375:             // Returns the length of the string written or FALSE
    376:             fputcsv($handle, $record, $delimiter, $enclosure);
    377:         }
    378:
    379:         // Reset the file pointer
    380:         rewind($handle);
    381:
    382:         // Retrieve the csv contents
    383:         $csv = stream_get_contents($handle);
    384:
    385:         // Close the handle
    386:         fclose($handle);
    387:
    388:         return $csv;
    389:     }
    390:
    391:     /**
    392:      * Encode data as json
    393:      *
    394:      * @param mixed|NULL $data Optional data to pass, so as to override the data passed
    395:      * to the constructor
    396:      * @return string Json representation of a value
    397:      */
    398:     public function to_json($data = NULL)
    399:     {
    400:         // If no data is passed as a parameter, then use the data passed
    401:         // via the constructor
    402:         if ($data === NULL && func_num_args() === 0)
    403:         {
    404:             $data = $this->_data;
    405:         }
    406:
    407:         // Get the callback parameter (if set)
    408:         $callback = $this->_CI->input->get('callback');
    409:
    410:         if (empty($callback) === TRUE)
    411:         {
    412:             return json_encode($data);
    413:         }
    414:
    415:         // We only honour a jsonp callback which are valid javascript identifiers
    416:         elseif (preg_match('/^[a-z_\$][a-z0-9\$_]*(\.[a-z_\$][a-z0-9\$_]*)*$/i', $callback))
    417:         {
    418:             // Return the data as encoded json with a callback
    419:             return $callback.'('.json_encode($data).');';
    420:         }
    421:
    422:         // An invalid jsonp callback function provided.
    423:         // Though I don't believe this should be hardcoded here
    424:         $data['warning'] = 'INVALID JSONP CALLBACK: '.$callback;
    425:
    426:         return json_encode($data);
    427:     }
    428:
    429:     /**
    430:      * Encode data as a serialized array
    431:      *
    432:      * @param mixed|NULL $data Optional data to pass, so as to override the data passed
    433:      * to the constructor
    434:      * @return string Serialized data
    435:      */
    436:     public function to_serialized($data = NULL)
    437:     {
    438:         // If no data is passed as a parameter, then use the data passed
    439:         // via the constructor
    440:         if ($data === NULL && func_num_args() === 0)
    441:         {
    442:             $data = $this->_data;
    443:         }
    444:
    445:         return serialize($data);
    446:     }
    447:
    448:     /**
    449:      * Format data using a PHP structure
    450:      *
    451:      * @param mixed|NULL $data Optional data to pass, so as to override the data passed
    452:      * to the constructor
    453:      * @return mixed String representation of a variable
    454:      */
    455:     public function to_php($data = NULL)
    456:     {
    457:         // If no data is passed as a parameter, then use the data passed
    458:         // via the constructor
    459:         if ($data === NULL && func_num_args() === 0)
    460:         {
    461:             $data = $this->_data;
    462:         }
    463:
    464:         return var_export($data, TRUE);
    465:     }
    466:
    467:     // INTERNAL FUNCTIONS
    468:
    469:     /**
    470:      * @param $data XML string
    471:      * @return SimpleXMLElement XML element object; otherwise, empty array
    472:      */
    473:     protected function _from_xml($data)
    474:     {
    475:         return $data ? (array) simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOCDATA) : [];
    476:     }
    477:
    478:     /**
    479:      * @param string $data CSV string
    480:      * @param string $delimiter The optional delimiter parameter sets the field
    481:      * delimiter (one character only). NULL will use the default value (,)
    482:      * @param string $enclosure The optional enclosure parameter sets the field
    483:      * enclosure (one character only). NULL will use the default value (")
    484:      * @return array A multi-dimensional array with the outer array being the number of rows
    485:      * and the inner arrays the individual fields
    486:      */
    487:     protected function _from_csv($data, $delimiter = ',', $enclosure = '"')
    488:     {
    489:         // If NULL, then set as the default delimiter
    490:         if ($delimiter === NULL)
    491:         {
    492:             $delimiter = ',';
    493:         }
    494:
    495:         // If NULL, then set as the default enclosure
    496:         if ($enclosure === NULL)
    497:         {
    498:             $enclosure = '"';
    499:         }
    500:
    501:         return str_getcsv($data, $delimiter, $enclosure);
    502:     }
    503:
    504:     /**
    505:      * @param $data Encoded json string
    506:      * @return mixed Decoded json string with leading and trailing whitespace removed
    507:      */
    508:     protected function _from_json($data)
    509:     {
    510:         return json_decode(trim($data));
    511:     }
    512:
    513:     /**
    514:      * @param string Data to unserialized
    515:      * @return mixed Unserialized data
    516:      */
    517:     protected function _from_serialize($data)
    518:     {
    519:         return unserialize(trim($data));
    520:     }
    521:
    522:     /**
    523:      * @param $data Data to trim leading and trailing whitespace
    524:      * @return string Data with leading and trailing whitespace removed
    525:      */
    526:     protected function _from_php($data)
    527:     {
    528:         return trim($data);
    529:     }
    530:
    531: }
    532: