PageRenderTime 42ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/classes/observer/typing.php

http://github.com/fuel/orm
PHP | 748 lines | 428 code | 76 blank | 244 comment | 53 complexity | 71e211ef178ebb2b62b701add6469eff MD5 | raw file
  1. <?php
  2. /**
  3. * Fuel is a fast, lightweight, community driven PHP 5.4+ framework.
  4. *
  5. * @package Fuel
  6. * @version 1.9-dev
  7. * @author Fuel Development Team
  8. * @license MIT License
  9. * @copyright 2010 - 2019 Fuel Development Team
  10. * @link https://fuelphp.com
  11. */
  12. namespace Orm;
  13. /**
  14. * Invalid content exception, thrown when type conversion is not possible.
  15. */
  16. class InvalidContentType extends \UnexpectedValueException {}
  17. /**
  18. * Typing observer.
  19. *
  20. * Runs on load or save, and ensures the correct data type of your ORM object properties.
  21. */
  22. class Observer_Typing
  23. {
  24. /**
  25. * @var array types of events to act on and whether they are pre- or post-database
  26. */
  27. public static $events = array(
  28. 'before_save' => 'before',
  29. 'after_save' => 'after',
  30. 'after_load' => 'after',
  31. );
  32. /**
  33. * @var array db type mappings
  34. */
  35. public static $type_mappings = array(
  36. 'tinyint' => 'int',
  37. 'smallint' => 'int',
  38. 'mediumint' => 'int',
  39. 'bigint' => 'int',
  40. 'integer' => 'int',
  41. 'double' => 'float',
  42. 'decimal' => 'float',
  43. 'tinytext' => 'text',
  44. 'mediumtext' => 'text',
  45. 'longtext' => 'text',
  46. 'boolean' => 'bool',
  47. 'time_unix' => 'time',
  48. 'time_mysql' => 'time',
  49. 'datetime' => 'time',
  50. 'date' => 'time',
  51. );
  52. /**
  53. * @var array db data types with the method(s) to use, optionally pre- or post-database
  54. */
  55. public static $type_methods = array(
  56. 'varchar' => array(
  57. 'before' => 'Orm\\Observer_Typing::type_string',
  58. ),
  59. 'int' => array(
  60. 'before' => 'Orm\\Observer_Typing::type_integer',
  61. 'after' => 'Orm\\Observer_Typing::type_integer',
  62. ),
  63. 'float' => array(
  64. 'before' => 'Orm\\Observer_Typing::type_float_before',
  65. 'after' => 'Orm\\Observer_Typing::type_float_after',
  66. ),
  67. 'text' => array(
  68. 'before' => 'Orm\\Observer_Typing::type_string',
  69. ),
  70. 'set' => array(
  71. 'before' => 'Orm\\Observer_Typing::type_set_before',
  72. 'after' => 'Orm\\Observer_Typing::type_set_after',
  73. ),
  74. 'enum' => array(
  75. 'before' => 'Orm\\Observer_Typing::type_set_before',
  76. ),
  77. 'bool' => array(
  78. 'before' => 'Orm\\Observer_Typing::type_bool_to_int',
  79. 'after' => 'Orm\\Observer_Typing::type_bool_from_int',
  80. ),
  81. 'serialize' => array(
  82. 'before' => 'Orm\\Observer_Typing::type_serialize',
  83. 'after' => 'Orm\\Observer_Typing::type_unserialize',
  84. ),
  85. 'encrypt' => array(
  86. 'before' => 'Orm\\Observer_Typing::type_encrypt',
  87. 'after' => 'Orm\\Observer_Typing::type_decrypt',
  88. ),
  89. 'json' => array(
  90. 'before' => 'Orm\\Observer_Typing::type_json_encode',
  91. 'after' => 'Orm\\Observer_Typing::type_json_decode',
  92. ),
  93. 'time' => array(
  94. 'before' => 'Orm\\Observer_Typing::type_time_encode',
  95. 'after' => 'Orm\\Observer_Typing::type_time_decode',
  96. ),
  97. );
  98. /**
  99. * @var array regexes for db types with the method(s) to use, optionally pre- or post-database
  100. */
  101. public static $regex_methods = array(
  102. '/^decimal:([0-9])/uiD' => array(
  103. 'before' => 'Orm\\Observer_Typing::type_decimal_before',
  104. 'after' => 'Orm\\Observer_Typing::type_decimal_after',
  105. ),
  106. );
  107. /**
  108. */
  109. public static $use_locale = true;
  110. /**
  111. * Make sure the orm config is loaded
  112. */
  113. public static function _init()
  114. {
  115. \Config::load('orm', true);
  116. static::$use_locale = \Config::get('orm.use_locale', static::$use_locale);
  117. }
  118. /**
  119. * Get notified of an event
  120. *
  121. * @param Model $instance
  122. * @param string $event
  123. */
  124. public static function orm_notify(Model $instance, $event)
  125. {
  126. // if we don't serve this event, bail out immediately
  127. if (array_key_exists($event, static::$events))
  128. {
  129. // get the event type of the event that triggered us
  130. $event_type = static::$events[$event];
  131. // fetch the model's properties
  132. $properties = $instance->properties();
  133. // and check if we need to do any datatype conversions
  134. foreach ($properties as $p => $settings)
  135. {
  136. // the property is part of the primary key, skip it
  137. if (in_array($p, $instance->primary_key()))
  138. {
  139. continue;
  140. }
  141. $instance->{$p} = static::typecast($p, $instance->{$p}, $settings, $event_type);
  142. }
  143. }
  144. }
  145. /**
  146. * Typecast a single column value based on the model properties for that column
  147. *
  148. * @param string $column name of the column
  149. * @param string $value value
  150. * @param string $settings column settings from the model
  151. *
  152. * @throws InvalidContentType
  153. *
  154. * @return mixed
  155. */
  156. public static function typecast($column, $value, $settings, $event_type = 'before')
  157. {
  158. // only on before_save, check if null is allowed
  159. if ($value === null)
  160. {
  161. // only on before_save
  162. if ($event_type == 'before')
  163. {
  164. if (array_key_exists('null', $settings) and $settings['null'] === false)
  165. {
  166. // if a default is defined, use that instead
  167. if (array_key_exists('default', $settings))
  168. {
  169. $value = $settings['default'];
  170. }
  171. else
  172. {
  173. throw new InvalidContentType('The property "'.$column.'" cannot be NULL.');
  174. }
  175. }
  176. }
  177. }
  178. // still null? then let the DB deal with it
  179. if ($value === null)
  180. {
  181. return $value;
  182. }
  183. // no datatype given
  184. if (empty($settings['data_type']))
  185. {
  186. return $value;
  187. }
  188. // get the data type for this column
  189. $data_type = $settings['data_type'];
  190. // is this a base data type?
  191. if ( ! isset(static::$type_methods[$data_type]))
  192. {
  193. // no, can we map it to one?
  194. if (isset(static::$type_mappings[$data_type]))
  195. {
  196. // yes, so swap it for a base data type
  197. $data_type = static::$type_mappings[$data_type];
  198. }
  199. else
  200. {
  201. // can't be mapped, check the regexes
  202. foreach (static::$regex_methods as $match => $methods)
  203. {
  204. // fetch the method
  205. $method = ! empty($methods[$event_type]) ? $methods[$event_type] : false;
  206. if ($method)
  207. {
  208. if (preg_match_all($match, $data_type, $matches) > 0)
  209. {
  210. $value = call_user_func($method, $value, $settings, $matches);
  211. }
  212. }
  213. }
  214. return $value;
  215. }
  216. }
  217. // fetch the method
  218. $method = ! empty(static::$type_methods[$data_type][$event_type]) ? static::$type_methods[$data_type][$event_type] : false;
  219. // if one was found, call it
  220. if ($method)
  221. {
  222. $value = call_user_func($method, $value, $settings);
  223. }
  224. return $value;
  225. }
  226. /**
  227. * Casts to string when necessary and checks if within max length
  228. *
  229. * @param mixed value to typecast
  230. * @param array any options to be passed
  231. *
  232. * @throws InvalidContentType
  233. *
  234. * @return string
  235. */
  236. public static function type_string($var, array $settings)
  237. {
  238. if (is_array($var) or (is_object($var) and ! method_exists($var, '__toString')))
  239. {
  240. throw new InvalidContentType('Array or object could not be converted to varchar.');
  241. }
  242. $var = strval($var);
  243. if (array_key_exists('character_maximum_length', $settings))
  244. {
  245. $length = intval($settings['character_maximum_length']);
  246. if ($length > 0 and strlen($var) > $length)
  247. {
  248. $var = substr($var, 0, $length);
  249. }
  250. }
  251. return $var;
  252. }
  253. /**
  254. * Casts to int when necessary and checks if within max values
  255. *
  256. * @param mixed value to typecast
  257. * @param array any options to be passed
  258. *
  259. * @throws InvalidContentType
  260. *
  261. * @return int
  262. */
  263. public static function type_integer($var, array $settings)
  264. {
  265. if (is_array($var) or is_object($var))
  266. {
  267. throw new InvalidContentType('Array or object could not be converted to integer.');
  268. }
  269. if ((array_key_exists('min', $settings) and $var < intval($settings['min']))
  270. or (array_key_exists('max', $settings) and $var > intval($settings['max'])))
  271. {
  272. throw new InvalidContentType('Integer value outside of range: '.$var);
  273. }
  274. return intval($var);
  275. }
  276. /**
  277. * Casts float to string when necessary
  278. *
  279. * @param mixed value to typecast
  280. *
  281. * @throws InvalidContentType
  282. *
  283. * @return float
  284. */
  285. public static function type_float_before($var, $settings = null)
  286. {
  287. if (is_array($var) or is_object($var))
  288. {
  289. throw new InvalidContentType('Array or object could not be converted to float.');
  290. }
  291. // do we need to do locale conversion?
  292. if (is_string($var) and static::$use_locale)
  293. {
  294. $locale_info = localeconv();
  295. $var = str_replace($locale_info["thousands_sep"], "", $var);
  296. $var = str_replace($locale_info["decimal_point"], ".", $var);
  297. }
  298. // was a specific float format specified?
  299. if (isset($settings['db_decimals']))
  300. {
  301. return sprintf('%.'.$settings['db_decimals'].'F', round((float) $var, $settings['db_decimals']));
  302. }
  303. if (isset($settings['data_type']) and strpos($settings['data_type'], 'decimal:') === 0)
  304. {
  305. $decimal = explode(':', $settings['data_type']);
  306. return sprintf('%.'.$decimal[1].'F', round((float) $var, $decimal[1]));
  307. }
  308. return $var;
  309. }
  310. /**
  311. * Casts to float when necessary
  312. *
  313. * @param mixed value to typecast
  314. *
  315. * @throws InvalidContentType
  316. *
  317. * @return float
  318. */
  319. public static function type_float_after($var)
  320. {
  321. if (is_array($var) or is_object($var))
  322. {
  323. throw new InvalidContentType('Array or object could not be converted to float.');
  324. }
  325. return floatval($var);
  326. }
  327. /**
  328. * Decimal pre-treater, converts a decimal representation to a float
  329. *
  330. * @param mixed value to typecast
  331. *
  332. * @throws InvalidContentType
  333. *
  334. * @return float
  335. */
  336. public static function type_decimal_before($var, $settings = null)
  337. {
  338. if (is_array($var) or is_object($var))
  339. {
  340. throw new InvalidContentType('Array or object could not be converted to decimal.');
  341. }
  342. return static::type_float_before($var, $settings);
  343. }
  344. /**
  345. * Decimal post-treater, converts any number to a decimal representation
  346. *
  347. * @param mixed value to typecast
  348. *
  349. * @throws InvalidContentType
  350. *
  351. * @return float
  352. */
  353. public static function type_decimal_after($var, array $settings, array $matches)
  354. {
  355. if (is_array($var) or is_object($var))
  356. {
  357. throw new InvalidContentType('Array or object could not be converted to decimal.');
  358. }
  359. if ( ! is_numeric($var))
  360. {
  361. throw new InvalidContentType('Value '.$var.' is not numeric and can not be converted to decimal.');
  362. }
  363. $dec = empty($matches[1][0]) ? 2 : $matches[1][0];
  364. // do we need to do locale aware conversion?
  365. if (static::$use_locale)
  366. {
  367. return sprintf("%.".$dec."f", round(static::type_float_after($var), $dec));
  368. }
  369. return sprintf("%.".$dec."F", round(static::type_float_after($var), $dec));
  370. }
  371. /**
  372. * Value pre-treater, deals with array values, and handles the enum type
  373. *
  374. * @param mixed value
  375. * @param array any options to be passed
  376. *
  377. * @throws InvalidContentType
  378. *
  379. * @return string
  380. */
  381. public static function type_set_before($var, array $settings)
  382. {
  383. $var = is_array($var) ? implode(',', $var) : strval($var);
  384. $values = array_filter(explode(',', trim($var)));
  385. if ($settings['data_type'] == 'enum' and count($values) > 1)
  386. {
  387. throw new InvalidContentType('Enum cannot have more than 1 value.');
  388. }
  389. foreach ($values as $val)
  390. {
  391. if ( ! in_array($val, $settings['options']))
  392. {
  393. throw new InvalidContentType('Invalid value given for '.ucfirst($settings['data_type']).
  394. ', value "'.$var.'" not in available options: "'.implode(', ', $settings['options']).'".');
  395. }
  396. }
  397. return $var;
  398. }
  399. /**
  400. * Value post-treater, converts a comma-delimited string into an array
  401. *
  402. * @param mixed value
  403. *
  404. * @return array
  405. */
  406. public static function type_set_after($var)
  407. {
  408. return explode(',', $var);
  409. }
  410. /**
  411. * Converts boolean input to 1 or 0 for the DB
  412. *
  413. * @param bool value
  414. *
  415. * @return int
  416. */
  417. public static function type_bool_to_int($var)
  418. {
  419. return $var ? 1 : 0;
  420. }
  421. /**
  422. * Converts DB bool values to PHP bool value
  423. *
  424. * @param bool value
  425. *
  426. * @return int
  427. */
  428. public static function type_bool_from_int($var)
  429. {
  430. return $var == '1' ? true : false;
  431. }
  432. /**
  433. * Returns the serialized input
  434. *
  435. * @param mixed value
  436. * @param array any options to be passed
  437. *
  438. * @throws InvalidContentType
  439. *
  440. * @return string
  441. */
  442. public static function type_serialize($var, array $settings)
  443. {
  444. $var = serialize($var);
  445. if (array_key_exists('character_maximum_length', $settings))
  446. {
  447. $length = intval($settings['character_maximum_length']);
  448. if ($length > 0 and strlen($var) > $length)
  449. {
  450. throw new InvalidContentType('Value could not be serialized, result exceeds max string length for field.');
  451. }
  452. }
  453. return $var;
  454. }
  455. /**
  456. * Unserializes the input
  457. *
  458. * @param string value
  459. *
  460. * @return mixed
  461. */
  462. public static function type_unserialize($var)
  463. {
  464. return empty($var) ? array() : unserialize($var);
  465. }
  466. /**
  467. * Returns the encrypted input
  468. *
  469. * @param mixed value
  470. * @param array any options to be passed
  471. *
  472. * @throws InvalidContentType
  473. *
  474. * @return string
  475. */
  476. public static function type_encrypt($var, array $settings)
  477. {
  478. // make the variable serialized, we need to be able to encrypt any variable type
  479. $var = static::type_serialize($var, $settings);
  480. // and encrypt it
  481. if (array_key_exists('encryption_key', $settings))
  482. {
  483. $var = \Crypt::encode($var, $settings['encryption_key']);
  484. }
  485. else
  486. {
  487. $var = \Crypt::encode($var);
  488. }
  489. // do a length check if needed
  490. if (array_key_exists('character_maximum_length', $settings))
  491. {
  492. $length = intval($settings['character_maximum_length']);
  493. if ($length > 0 and strlen($var) > $length)
  494. {
  495. throw new InvalidContentType('Value could not be encrypted, result exceeds max string length for field.');
  496. }
  497. }
  498. return $var;
  499. }
  500. /**
  501. * decrypt the input
  502. *
  503. * @param string value
  504. *
  505. * @return mixed
  506. */
  507. public static function type_decrypt($var)
  508. {
  509. // decrypt it
  510. if (array_key_exists('encryption_key', $settings))
  511. {
  512. $var = \Crypt::decode($var, $settings['encryption_key']);
  513. }
  514. else
  515. {
  516. $var = \Crypt::decode($var);
  517. }
  518. return $var;
  519. }
  520. /**
  521. * JSON encodes the input
  522. *
  523. * @param mixed value
  524. * @param array any options to be passed
  525. *
  526. * @throws InvalidContentType
  527. *
  528. * @return string
  529. */
  530. public static function type_json_encode($var, array $settings)
  531. {
  532. $var = json_encode($var);
  533. if (array_key_exists('character_maximum_length', $settings))
  534. {
  535. $length = intval($settings['character_maximum_length']);
  536. if ($length > 0 and strlen($var) > $length)
  537. {
  538. throw new InvalidContentType('Value could not be JSON encoded, exceeds max string length for field.');
  539. }
  540. }
  541. return $var;
  542. }
  543. /**
  544. * Decodes the JSON
  545. *
  546. * @param string value
  547. *
  548. * @return mixed
  549. */
  550. public static function type_json_decode($var, $settings)
  551. {
  552. $assoc = false;
  553. if (array_key_exists('json_assoc', $settings))
  554. {
  555. $assoc = (bool) $settings['json_assoc'];
  556. }
  557. return json_decode($var, $assoc);
  558. }
  559. /**
  560. * Takes a Date instance and transforms it into a DB timestamp
  561. *
  562. * @param \Fuel\Core\Date value
  563. * @param array any options to be passed
  564. *
  565. * @throws InvalidContentType
  566. *
  567. * @return int|string
  568. */
  569. public static function type_time_encode(\Fuel\Core\Date $var, array $settings)
  570. {
  571. if ( ! $var instanceof \Fuel\Core\Date)
  572. {
  573. throw new InvalidContentType('Value must be an instance of the Date class.');
  574. }
  575. // deal with datetime values
  576. elseif ($settings['data_type'] == 'datetime')
  577. {
  578. return $var->format('%Y-%m-%d %H:%M:%S');
  579. }
  580. // deal with date values
  581. elseif ($settings['data_type'] == 'date')
  582. {
  583. return $var->format('%Y-%m-%d');
  584. }
  585. // deal with time values
  586. elseif ($settings['data_type'] == 'time')
  587. {
  588. return $var->format('%H:%M:%S');
  589. }
  590. // deal with config defined timestamps
  591. elseif ($settings['data_type'] == 'time_mysql')
  592. {
  593. return $var->format('mysql');
  594. }
  595. // assume a timestamo is required
  596. return $var->get_timestamp();
  597. }
  598. /**
  599. * Takes a DB timestamp and converts it into a Date object
  600. *
  601. * @param string value
  602. * @param array any options to be passed
  603. *
  604. * @return \Fuel\Core\Date
  605. */
  606. public static function type_time_decode($var, array $settings)
  607. {
  608. // deal with a 'nulled' date, which according to some RDMBS is a valid enough to store?
  609. if ($var == '0000-00-00 00:00:00')
  610. {
  611. if (array_key_exists('null', $settings) and $settings['null'] === false)
  612. {
  613. throw new InvalidContentType('Value '.$var.' is not a valid date and can not be converted to a Date object.');
  614. }
  615. return null;
  616. }
  617. // deal with datetime values
  618. elseif ($settings['data_type'] == 'datetime')
  619. {
  620. try
  621. {
  622. $var = \Date::create_from_string($var, '%Y-%m-%d %H:%M:%S');
  623. }
  624. catch (\UnexpectedValueException $e)
  625. {
  626. throw new InvalidContentType('Value '.$var.' is not a valid datetime and can not be converted to a Date object.');
  627. }
  628. }
  629. // deal with date values
  630. elseif ($settings['data_type'] == 'date')
  631. {
  632. try
  633. {
  634. $var = \Date::create_from_string($var, '%Y-%m-%d');
  635. }
  636. catch (\UnexpectedValueException $e)
  637. {
  638. throw new InvalidContentType('Value '.$var.' is not a valid date and can not be converted to a Date object.');
  639. }
  640. }
  641. // deal with time values
  642. elseif ($settings['data_type'] == 'time')
  643. {
  644. try
  645. {
  646. $var = \Date::create_from_string($var, '%H:%M:%S');
  647. }
  648. catch (\UnexpectedValueException $e)
  649. {
  650. throw new InvalidContentType('Value '.$var.' is not a valid time and can not be converted to a Date object.');
  651. }
  652. }
  653. // deal with a configured datetime value
  654. elseif ($settings['data_type'] == 'time_mysql')
  655. {
  656. try
  657. {
  658. $var = \Date::create_from_string($var, 'mysql');
  659. }
  660. catch (\UnexpectedValueException $e)
  661. {
  662. throw new InvalidContentType('Value '.$var.' is not a valid mysql datetime and can not be converted to a Date object.');
  663. }
  664. }
  665. // else assume it is a numeric timestamp
  666. else
  667. {
  668. $var = \Date::forge($var);
  669. }
  670. return $var;
  671. }
  672. }