PageRenderTime 50ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/wp-content/plugins/the-events-calendar/src/Tribe/Cost_Utils.php

https://gitlab.com/ezgonzalez/integral
PHP | 506 lines | 257 code | 80 blank | 169 comment | 37 complexity | 7a2f146221ae6bf7ba51edd2c98f3603 MD5 | raw file
  1. <?php
  2. /**
  3. * Cost utility functions
  4. */
  5. // Don't load directly
  6. if ( ! defined( 'ABSPATH' ) ) {
  7. die( '-1' );
  8. }
  9. class Tribe__Events__Cost_Utils {
  10. const UNCOSTED_EVENTS_TRANSIENT = 'tribe_events_have_uncosted_events';
  11. /**
  12. * @var string
  13. */
  14. protected $_current_original_cost_separator;
  15. /**
  16. * @var string
  17. */
  18. protected $_supported_decimal_separators = '.,';
  19. /**
  20. * Static Singleton Factory Method
  21. *
  22. * @return Tribe__Events__Cost_Utils
  23. */
  24. public static function instance() {
  25. static $instance;
  26. if ( ! $instance ) {
  27. $instance = new self;
  28. }
  29. return $instance;
  30. }
  31. /**
  32. * fetches all event costs from the database
  33. *
  34. * @return array
  35. */
  36. public function get_all_costs() {
  37. global $wpdb;
  38. $costs = $wpdb->get_col( "
  39. SELECT
  40. DISTINCT meta_value
  41. FROM
  42. {$wpdb->postmeta}
  43. WHERE
  44. meta_key = '_EventCost'
  45. AND LENGTH( meta_value ) > 0;
  46. " );
  47. return $this->parse_cost_range( $costs );
  48. }
  49. /**
  50. * Fetch the possible separators
  51. *
  52. * @return array
  53. */
  54. public function get_separators() {
  55. /**
  56. * Allow users to create more possible separators, they must be only 1 char
  57. *
  58. * @var array
  59. */
  60. return apply_filters( 'tribe_events_cost_separators', array( ',', '.' ) );
  61. }
  62. public function get_cost_regex() {
  63. $separators = '[\\' . implode( '\\', $this->get_separators() ) . ']?';
  64. return apply_filters( 'tribe_events_cost_regex',
  65. '(' . $separators . '([\d]+)' . $separators . '([\d]*))' );
  66. }
  67. /**
  68. * Check if a String is a valid cost
  69. *
  70. * @param string $cost String to be checked
  71. *
  72. * @return boolean
  73. */
  74. public function is_valid_cost( $cost, $allow_negative = true ) {
  75. return preg_match( $this->get_cost_regex(), trim( $cost ) );
  76. }
  77. /**
  78. * fetches an event's cost values
  79. *
  80. * @param int|WP_Post $event The Event post object or event ID
  81. *
  82. * @return array
  83. */
  84. public function get_event_costs( $event ) {
  85. $event = get_post( $event );
  86. if ( ! is_object( $event ) || ! $event instanceof WP_Post ) {
  87. return array();
  88. }
  89. if ( ! tribe_is_event( $event->ID ) ) {
  90. return array();
  91. }
  92. $costs = tribe_get_event_meta( $event->ID, '_EventCost', false );
  93. $parsed_costs = array();
  94. foreach ( $costs as $index => $value ) {
  95. if ( '' === $value ) {
  96. continue;
  97. }
  98. $parsed_costs += $this->parse_cost_range( $value );
  99. }
  100. return $parsed_costs;
  101. }
  102. /**
  103. * Returns a formatted event cost
  104. *
  105. * @param int|WP_Post $event The Event post object or event ID
  106. * @param bool $with_currency_symbol Include the currency symbol (optional)
  107. *
  108. * @return string
  109. */
  110. public function get_formatted_event_cost( $event, $with_currency_symbol = false ) {
  111. $costs = $this->get_event_costs( $event );
  112. if ( ! $costs ) {
  113. return '';
  114. }
  115. $relevant_costs = array(
  116. 'min' => $this->get_cost_by_func( $costs, 'min' ),
  117. 'max' => $this->get_cost_by_func( $costs, 'max' ),
  118. );
  119. foreach ( $relevant_costs as &$cost ) {
  120. $cost = $this->maybe_replace_cost_with_free( $cost );
  121. if ( $with_currency_symbol ) {
  122. $cost = $this->maybe_format_with_currency( $cost );
  123. }
  124. $cost = esc_html( $cost );
  125. }
  126. if ( $relevant_costs['min'] == $relevant_costs['max'] ) {
  127. $formatted = $relevant_costs['min'];
  128. } else {
  129. $formatted = $relevant_costs['min'] . _x( ' - ',
  130. 'Cost range separator',
  131. 'the-events-calendar' ) . $relevant_costs['max'];
  132. }
  133. return $formatted;
  134. }
  135. /**
  136. * If the cost is "0", call it "Free"
  137. *
  138. * @param int|float|string $cost Cost to analyze
  139. *
  140. * return int|float|string
  141. */
  142. public function maybe_replace_cost_with_free( $cost ) {
  143. if ( '0' === (string) $cost ) {
  144. return esc_html__( 'Free', 'the-events-calendar' );
  145. }
  146. return $cost;
  147. }
  148. /**
  149. * Formats a cost with a currency symbol
  150. *
  151. * @param int|float|string $cost Cost to format
  152. *
  153. * return string
  154. */
  155. public function maybe_format_with_currency( $cost ) {
  156. // check if the currency symbol is desired, and it's just a number in the field
  157. // be sure to account for european formats in decimals, and thousands separators
  158. if ( is_numeric( str_replace( $this->get_separators(), '', $cost ) ) ) {
  159. $cost = tribe_format_currency( $cost );
  160. }
  161. return $cost;
  162. }
  163. /**
  164. * Returns a particular cost within an array of costs
  165. *
  166. * @param $costs mixed Cost(s) to review for max value
  167. * @param $function string Function to use to determine which cost to return from range. Valid values: max, min
  168. *
  169. * @return float
  170. */
  171. protected function get_cost_by_func( $costs = null, $function = 'max' ) {
  172. if ( null === $costs ) {
  173. $costs = $this->get_all_costs();
  174. } else {
  175. $costs = (array) $costs;
  176. }
  177. $costs = $this->parse_cost_range( $costs );
  178. // if there's only one item, we're looking at a single event. If the cost is non-numeric, let's
  179. // return the non-numeric cost so that value is preserved
  180. if ( 1 === count( $costs ) && ! is_numeric( current( $costs ) ) ) {
  181. return current( $costs );
  182. }
  183. // make sure we are only trying to get numeric min/max values
  184. $costs = array_filter( $costs, 'is_numeric' );
  185. if ( empty( $costs ) ) {
  186. return 0;
  187. }
  188. switch ( $function ) {
  189. case 'min':
  190. $cost = $costs[ min( array_keys( $costs ) ) ];
  191. break;
  192. case 'max':
  193. default:
  194. $cost = $costs[ max( array_keys( $costs ) ) ];
  195. break;
  196. }
  197. // If there isn't anything on the cost just return 0
  198. if ( empty( $cost ) ) {
  199. return 0;
  200. }
  201. return $cost;
  202. }
  203. /**
  204. * Returns a maximum cost in a list of costs. If an array of costs is not passed in, the array of costs is fetched
  205. * via query.
  206. *
  207. * @param $costs mixed Cost(s) to review for max value
  208. *
  209. * @return float
  210. */
  211. public function get_maximum_cost( $costs = null ) {
  212. return $this->get_cost_by_func( $costs, 'max' );
  213. }
  214. /**
  215. * Returns a minimum cost in a list of costs. If an array of costs is not passed in, the array of costs is fetched
  216. * via query.
  217. *
  218. * @param $costs mixed Cost(s) to review for min value
  219. *
  220. * @return float
  221. */
  222. public function get_minimum_cost( $costs = null ) {
  223. return $this->get_cost_by_func( $costs, 'min' );
  224. }
  225. /**
  226. * Returns boolean true if there are events for which a cost has not been specified.
  227. *
  228. * @return bool
  229. */
  230. public function has_uncosted_events() {
  231. global $wpdb;
  232. // Expect: false := not set/expired, 1 := have uncosted events, 0 := no uncosted events
  233. $have_uncosted = get_transient( self::UNCOSTED_EVENTS_TRANSIENT );
  234. if ( false !== $have_uncosted ) {
  235. return (bool) $have_uncosted;
  236. }
  237. // @todo consider expanding our logic for improved handling of private posts etc
  238. $uncosted = $wpdb->get_var( $wpdb->prepare( "
  239. SELECT ID
  240. FROM {$wpdb->posts}
  241. LEFT JOIN {$wpdb->postmeta}
  242. ON ( post_id = ID AND meta_key = '_EventCost' )
  243. WHERE post_type = %s
  244. AND (
  245. LENGTH( meta_value ) = 0
  246. OR meta_value IS NULL
  247. )
  248. AND post_status NOT IN ( 'auto-draft', 'revision' )
  249. LIMIT 1
  250. ", Tribe__Events__Main::POSTTYPE ) );
  251. /**
  252. * Whether or not we currently have events without any costs is something we store
  253. * in a transient to avoid repeated queries: this filter controls how long in seconds
  254. * that transient is allowed to live for.
  255. *
  256. * @param int $expires_after
  257. */
  258. $expire_after = apply_filters( 'tribe_events_cost_utils_uncosted_events_expiry', HOUR_IN_SECONDS );
  259. // We cast to an int to avoid confusion when we next check the transient
  260. // (since bool false will be returned when the transient is not set)
  261. set_transient( self::UNCOSTED_EVENTS_TRANSIENT, (int) $uncosted, $expire_after );
  262. return (bool) ( $uncosted > 0 );
  263. }
  264. /**
  265. * Parses an event cost into an array of ranges. If a range isn't provided, the resulting array will hold a single
  266. * value.
  267. *
  268. * @param $cost string Cost for event.
  269. *
  270. * @return array
  271. */
  272. public function parse_cost_range( $costs, $max_decimals = null ) {
  273. if ( ! is_array( $costs ) && ! is_string( $costs ) ) {
  274. return array();
  275. }
  276. // make sure costs is an array
  277. $costs = (array) $costs;
  278. // If there aren't any costs, return a blank array
  279. if ( 0 === count( $costs ) ) {
  280. return array();
  281. }
  282. // Build the regular expression
  283. $price_regex = $this->get_cost_regex();
  284. $max = 0;
  285. foreach ( $costs as &$cost ) {
  286. // Get the required parts
  287. if ( preg_match_all( '/' . $price_regex . '/', $cost, $matches ) ) {
  288. $cost = reset( $matches );
  289. } else {
  290. $cost = array( $cost );
  291. continue;
  292. }
  293. // Get the max number of decimals for the range
  294. if ( count( $matches ) === 4 ) {
  295. $decimals = max( array_map( 'strlen', end( $matches ) ) );
  296. $max = max( $max, $decimals );
  297. }
  298. }
  299. // If we passed max decimals
  300. if ( ! is_null( $max_decimals ) ) {
  301. $max = max( $max_decimals, $max );
  302. }
  303. $ocost = array();
  304. $costs = call_user_func_array( 'array_merge', $costs );
  305. foreach ( $costs as $cost ) {
  306. $numeric_cost = str_replace( $this->get_separators(), '.', $cost );
  307. if ( is_numeric( $numeric_cost ) ) {
  308. // Creates a Well Balanced Index that will perform good on a Key Sorting method
  309. $index = str_replace( array( '.', ',' ), '', number_format( $numeric_cost, $max ) );
  310. } else {
  311. // Makes sure that we have "index-safe" string
  312. $index = sanitize_title( $numeric_cost );
  313. }
  314. // Keep the Costs in a organizeable array by keys with the "numeric" value
  315. $ocost[ $index ] = $cost;
  316. }
  317. // Filter keeping the Keys
  318. ksort( $ocost );
  319. return (array) $ocost;
  320. }
  321. /**
  322. * @param string $original_string_cost A string cost with or without currency symbol,
  323. * e.g. `10 - 20`, `Free` or `2$ - 4$`.
  324. * @param array|string $merging_cost A single string cost representation to merge or an array of
  325. * string cost representations to merge, e.g. ['Free', 10, 20,
  326. * 'Donation'] or `Donation`.
  327. * @param bool $with_currency_symbol Whether the output should prepend the currency symbol to the
  328. * numeric costs or not.
  329. * @param array $sorted_mins An array of non numeric price minimums sorted smaller to larger,
  330. * e.g. `['Really free', 'Somewhat free', 'Free with 3 friends']`.
  331. * @param array $sorted_maxs An array of non numeric price maximums sorted smaller to larger,
  332. * e.g. `['Donation min $10', 'Donation min $20', 'Donation min
  333. * $100']`.
  334. *
  335. * @return string|array The merged cost range.
  336. */
  337. public function merge_cost_ranges( $original_string_cost, $merging_cost, $with_currency_symbol, $sorted_mins = array(), $sorted_maxs = array() ) {
  338. if ( empty( $merging_cost ) || $original_string_cost === $merging_cost ) {
  339. return $original_string_cost;
  340. }
  341. $_merging_cost = array_map( array( $this, 'convert_decimal_separator' ),
  342. (array) $merging_cost );
  343. $_merging_cost = array_map( array( $this, 'numerize_numbers' ), $_merging_cost );
  344. $numeric_merging_cost_costs = array_filter( $_merging_cost, 'is_numeric' );
  345. $matches = array();
  346. preg_match_all( '!\d+(?:([' . preg_quote( $this->_supported_decimal_separators ) . '])\d+)?!',
  347. $original_string_cost,
  348. $matches );
  349. $this->_current_original_cost_separator = empty( $matches[1][0] ) ? '.' : $matches[1][0];
  350. $matches[0] = empty( $matches[0] ) ? $matches[0] : array_map( array(
  351. $this,
  352. 'convert_decimal_separator',
  353. ),
  354. $matches[0] );
  355. $numeric_orignal_costs = empty( $matches[0] ) ? $matches[0] : array_map( 'floatval',
  356. $matches[0] );
  357. $all_numeric_costs = array_filter( array_merge( $numeric_merging_cost_costs, $numeric_orignal_costs ) );
  358. $cost_min = $cost_max = false;
  359. $merging_mins = array_intersect( $sorted_mins, (array) $merging_cost );
  360. $merging_has_min = array_search( reset( $merging_mins ), $sorted_mins );
  361. $original_has_min = array_search( $original_string_cost, $sorted_mins );
  362. $merging_has_min = false === $merging_has_min ? 999 : $merging_has_min;
  363. $original_has_min = false === $original_has_min ? 999 : $original_has_min;
  364. $string_min_key = min( $merging_has_min, $original_has_min );
  365. if ( array_key_exists( $string_min_key, $sorted_mins ) ) {
  366. $cost_min = $sorted_mins[ $string_min_key ];
  367. } else {
  368. $cost_min = empty( $all_numeric_costs ) ? '' : min( $all_numeric_costs );
  369. }
  370. $merging_maxs = array_intersect( $sorted_maxs, (array) $merging_cost );
  371. $merging_has_max = array_search( end( $merging_maxs ), $sorted_maxs );
  372. $original_has_max = array_search( $original_string_cost, $sorted_maxs );
  373. $merging_has_max = false === $merging_has_max ? - 1 : $merging_has_max;
  374. $original_has_max = false === $original_has_max ? - 1 : $original_has_max;
  375. $string_max_key = max( $merging_has_max, $original_has_max );
  376. if ( array_key_exists( $string_max_key, $sorted_maxs ) ) {
  377. $cost_max = $sorted_maxs[ $string_max_key ];
  378. } else {
  379. $cost_max = empty( $all_numeric_costs ) ? '' : max( $all_numeric_costs );
  380. }
  381. $cost = array_filter( array( $cost_min, $cost_max ) );
  382. if ( $with_currency_symbol ) {
  383. $formatted_cost = array();
  384. foreach ( $cost as $c ) {
  385. $formatted_cost[] = is_numeric( $c ) ? tribe_format_currency( $c ) : $c;
  386. }
  387. $cost = $formatted_cost;
  388. }
  389. return empty( $cost ) ? $original_string_cost : array_map( array( $this, 'restore_original_decimal_separator' ),
  390. $cost );
  391. }
  392. /**
  393. * Converts the original decimal separator to ".".
  394. *
  395. * @param string|int $value
  396. *
  397. * @return string
  398. */
  399. protected function convert_decimal_separator( $value ) {
  400. return preg_replace( '/[' . preg_quote( $this->_supported_decimal_separators ) . ']/', '.', $value );
  401. }
  402. /**
  403. * Restores the decimal separator to its original symbol.
  404. *
  405. * @param string $value
  406. *
  407. * @return string
  408. */
  409. private function restore_original_decimal_separator( $value ) {
  410. return str_replace( '.', $this->_current_original_cost_separator, $value );
  411. }
  412. /**
  413. * Extracts int and floats from a numeric "dirty" string like strings that might contain other symbols.
  414. *
  415. * E.g. "$10" will yield "10"; "23.55$" will yield "23.55".
  416. *
  417. * @param string|int $value
  418. *
  419. * @return int|float
  420. */
  421. private function numerize_numbers( $value ) {
  422. $matches = array();
  423. $pattern = '/(\\d{1,}([' . $this->_supported_decimal_separators . ']\\d{1,}))/';
  424. return preg_match( $pattern, $value, $matches ) ? $matches[1] : $value;
  425. }
  426. }