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: