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

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

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