You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

530 lines
14 KiB

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